package edu.uci.ics.multicast.host;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class MPeer {

//	private List<Socket> clientsSocket = Collections
//	.synchronizedList(new LinkedList<Socket>());
	private ServerSocket serverSocket;
	private String bindAddress;

	private List<Socket> peersSocket = Collections
			.synchronizedList(new LinkedList<Socket>());

	private static final int REMOTE_PORT = 51111;
	private static final int STREAMING_INTERVAL = 500;

	private boolean running = false;
	private boolean failure = false;
	// private String lastDataRecvd;
	private long maxDelay = -1;

	public MPeer(String bindAddress) throws UnknownHostException, IOException {
		running = true;
		this.bindAddress = bindAddress;
		listenForIncommingConnections();
	}

	private void listenForIncommingConnections() throws UnknownHostException,
			IOException {
		serverSocket = new ServerSocket();
		serverSocket.bind(new InetSocketAddress(InetAddress
				.getByName(bindAddress), REMOTE_PORT));
		System.out.println("Client "
				+ getIPAddress(serverSocket.getLocalSocketAddress())
				+ " is waiting for connections...");

		Runnable server = new Runnable() {
			public void run() {
				while (running) {
					try {
						Socket socket = serverSocket.accept();
						peersSocket.add(socket);
						System.out.println("Client "
								+ getIPAddress(socket.getRemoteSocketAddress())
								+ " is connected to "
								+ getIPAddress(socket.getLocalSocketAddress()));
						waitForData(socket);
					} catch (SocketException e) {

					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		};
		new Thread(server).start();
	}

	private synchronized void setMaxDelay(long delay){
		this.maxDelay = delay;
	}
	
	private void waitForData(final Socket socket) {
		Runnable client = new Runnable() {
			public void run() {
				boolean disconnected = false;
				while (!disconnected && running) {
					try {
						ObjectInputStream ois = new ObjectInputStream(
								socket.getInputStream());
						String data = (String) ois.readObject();
						long delay = System.currentTimeMillis()
								- Long.parseLong(data);
						if (delay > maxDelay) {
							setMaxDelay(delay);
						}
//						 System.out.println("Client "
//						 + getIPAddress(socket.getLocalSocketAddress())
//						 + " received data from "
//						 + getIPAddress(socket.getRemoteSocketAddress())
//						 + " content: " + data);
						if (!failure) {
							for (Iterator<Socket> itChild = peersSocket.iterator(); itChild
									.hasNext();) {
								Socket child = itChild.next();
								if (!child.equals(socket)) {
								try {
									ObjectOutputStream oos = new ObjectOutputStream(
											child.getOutputStream());
									oos.writeObject(data);
									oos.flush();
//									System.out.println("Fowarding to "+getIPAddress(child.getRemoteSocketAddress()));
								} catch (SocketException e) {
									System.out.println("Child "
											+ getIPAddress(child
													.getRemoteSocketAddress())
											+ " disconnected");
									child.close();
									itChild.remove();
								}
								} else {
//									System.out.println("Skipping "+getIPAddress(child.getRemoteSocketAddress()));
								}
							}
						}

					} catch (Exception e) {
						disconnected = true;
					}
				}
			}
		};
		new Thread(client).start();
	}

	public void connect(String[] hostnames, long waitInterval) throws UnknownHostException,
			IOException, InterruptedException {
//		System.out.println(hostnames[0]+" "+hostnames[1]);
		Thread.sleep(waitInterval);
		for(String hostname : hostnames) {
			try {
			System.out.println("Client "+bindAddress+" connecting to "+hostname);
			Socket clientSocket = new Socket(hostname, REMOTE_PORT,
					InetAddress.getByName(bindAddress), 0);
			peersSocket.add(clientSocket);
			waitForData(clientSocket);		
			} catch (ConnectException e){
				System.out.println("Client "+bindAddress+" unable to connect to "+hostname);
			}
		}
		

	}

	public void streamDataToChilds(final long interval) throws InterruptedException {
		// int seqNo = -1;
		while (running) {
			if (peersSocket.size() > 0) {
				// seqNo++;

				// for (Socket child : childsSocket) {
				for (Iterator<Socket> itChild = peersSocket.iterator(); itChild
						.hasNext();) {
					Socket child = itChild.next();

					try {
						// System.out
						// .println("Client "
						// + getIPAddress(child
						// .getLocalSocketAddress())
						// + " is sending data to "
						// + getIPAddress(child
						// .getRemoteSocketAddress())
						// + " "
						// + child.getInetAddress()
						// .getHostAddress());
						try {
							ObjectOutputStream oos = new ObjectOutputStream(
									child.getOutputStream());
							oos.writeObject(String.valueOf(System
									.currentTimeMillis()));
							oos.flush();
						} catch (SocketException e) {
							System.out.println("Child "
									+ getIPAddress(child
											.getRemoteSocketAddress())
									+ " disconnected");
							child.close();
							itChild.remove();
						}
					} catch (IOException e) {

					}
				}
			}
			Thread.sleep(interval);
		}
	}

	public void setShutdownIn(final long milliseconds) {
		final MPeer peer = this;
		Runnable timer = new Runnable() {
			public void run() {
				try {
					Thread.sleep(milliseconds);
					// System.out.println("Client "
					// + getIPAddress(peer.serverSocket
					// .getLocalSocketAddress())
					// + " is shutdown... ");
					running = false;
					peer.serverSocket.close();
					for(Socket client : peer.peersSocket) {
						client.close();
					}
					System.out.println("Client "
							+ getIPAddress(peer.serverSocket
									.getLocalSocketAddress()) + " maxDelay: "
							+ maxDelay);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		};
		new Thread(timer).start();
	}

	public void setFailureIn(final long milliseconds) {
		final MPeer peer = this;
		Runnable timer = new Runnable() {
			public void run() {
				try {
					Thread.sleep(milliseconds);
					System.out.println("Client "
							+ getIPAddress(peer.serverSocket
									.getLocalSocketAddress())
							+ " is now with failure... ");
					failure = true;
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		};
		new Thread(timer).start();
	}

	private String getIPAddress(SocketAddress sa) {
		String result = "";
		if (sa instanceof InetSocketAddress) {
			InetSocketAddress inetAddress = (InetSocketAddress) sa;
			result = inetAddress.getAddress().getHostAddress();
		}
		return result;
	}

	public static void main(String argv[]) throws Exception {
		// Print how to use the program
		if (argv.length < 2) {
			help();
		} else {
			List<String> argList = Arrays.asList(argv);

			try {
				String bindAddress = argList.get(argList.indexOf("-b") + 1)
						.trim();
				boolean isStreaming = argList.indexOf("-s") > -1 ? true : false;
				final long shutdownInterval = argList.indexOf("-t") > -1 ? Long
						.parseLong(argList.get(argList.indexOf("-t") + 1)
								.trim()) : -1;
				final long failureInterval = argList.indexOf("-f") > -1 ? Long
						.parseLong(argList.get(argList.indexOf("-f") + 1)
								.trim()) : -1;
						final long waitInterval = argList.indexOf("-w") > -1 ? Long
								.parseLong(argList.get(argList.indexOf("-w") + 1)
										.trim()) : -1;
				String hosts = argList.indexOf("-h") > -1 ? argList.get(
						argList.indexOf("-h") + 1).trim() : null;
						
		

				final MPeer peer = new MPeer(bindAddress);

				if (hosts != null) {
					String[] hostsArray = hosts.split(",");
					peer.connect(hostsArray, waitInterval);
				}

				if (shutdownInterval > -1) {
					peer.setShutdownIn(shutdownInterval);
				}

				if (failureInterval > -1) {
					peer.setShutdownIn(shutdownInterval);
				}

				if (isStreaming) {
					long interval = Long
					.parseLong(argList.get(argList.indexOf("-s") + 1));
					peer.streamDataToChilds(interval);
				}

			} catch (ArrayIndexOutOfBoundsException e) {
				System.err
						.println("Argument -b is required. Check help() for instructions.");
				e.printStackTrace();
			}
		}
	}

	private static void help() {
		System.out.println("Usage: -b bind_address [-h hostname] [-s]");
		System.out.println("where:");
		System.out.println("	-b defines the ip address to bind");
		System.out
				.println("	-h defines the remote hostname to connect (optional)");
		System.out.println("	-s defines if the peer streams data (optional)");
	}
}
